/**
 * 
 */
package jnzb.control;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;

import jnzb.control.events.InfoEventProcessor;
import jnzb.control.util.FileUtils;
import jnzb.model.nzb.File;
import jnzb.model.nzb.Segment;
import ar.com.ktulu.yenc.SizeException;
import ar.com.ktulu.yenc.YEncDecoder;
import ar.com.ktulu.yenc.YEncException;
import ar.com.ktulu.yenc.YEncMissingTrailerException;

/**
 * Object responsible for decoding
 * 
 * @author Rid
 * 
 */
public class Decoder implements Runnable {

	// 64K input buffer for segments when decoding
	private static final int DECODE_BUFFER_SIZE = 1024 * 64;

	// 1 second between checks of the empty queue
	private static int SLEEP_PERIOD = 1000 * 1;

	private Controller controller;

	private LinkedList<File> queue = new LinkedList<File>();

	private boolean keepGoing = true;

	private File currentFile;

	private YEncDecoder ydecoder = new YEncDecoder();

	private NZBProcessor nzbProcessor;

	private InfoEventProcessor infoProcessor;

	private Configuration config;

	public Decoder(Controller controller, NZBProcessor nzbProcessor,
			InfoEventProcessor infoProcessor) {
		this.controller = controller;
		this.nzbProcessor = nzbProcessor;
		this.infoProcessor = infoProcessor;
		this.config = Configuration.getInstance();
		// Kick off the decoder monitor
		new Thread(this).start();
	}

	public void stop() {
		this.keepGoing = false;
	}

	/**
	 * Adds an NZB file element to the decoder queue.
	 * 
	 * @param file
	 */
	public void addFile(File file) {
		synchronized (queue) {
			queue.addLast(file);
			infoProcessor.debug(file.getOutputFilename() + " added to decode queue");
		}
	}

	@Override
	public void run() {
		while (keepGoing) {
			if (this.queue.size() > 0) {
				synchronized (queue) {
					currentFile = queue.removeFirst();
				}
				decodeFile();
				nzbProcessor.decodeComplete(currentFile);
			} else {
				try {
					// change to wait, then use notify to wake up
					Thread.sleep(SLEEP_PERIOD);
				} catch (InterruptedException e) {
					// log an error here
					e.printStackTrace();
				}
			}
		}
		infoProcessor.info("Decoder thread exiting");
	}

	/**
	 * Decodes the currentFile using a YY decoder
	 */
	private void decodeFile() {
		// Ensure decoder is ready
		ydecoder.reset();

		if (currentFile.allSegmentsDownloaded()) {

			FileOutputStream fos = null;
			BufferedOutputStream bos = null;
			FileInputStream fis = null;
			BufferedInputStream bis = null;
			String outputFilename = null;

			try {
				// Pass in the first segment to lookup the filename
				outputFilename = getOutputFilename(currentFile.getSegments()
						.get(0));

				if (outputFilename == null) {
					outputFilename = "unknownFilename.dat";
					infoProcessor
							.error("Could not get target filename - file might be incorrect. Subject="
									+ currentFile.getSubject());
				}
				String outputPath = config.getPartsPath(currentFile
						.getRootNZB().getFilename());

				currentFile.setOutputFilename(outputFilename);
				currentFile.setAbsolutePath(outputPath);
				
				currentFile.setDecoded(false);
				fos = new FileOutputStream(outputPath + outputFilename);
				bos = new BufferedOutputStream(fos, DECODE_BUFFER_SIZE);
				ydecoder.setOutputStream(bos);
				Iterator<Segment> segments = currentFile.getSegments()
						.iterator();

				infoProcessor.info("Decoding file " + outputFilename);

				while (segments.hasNext()) {
					Segment segment = segments.next();

					fis = new FileInputStream(segment
							.getDownloadedSegmentFilename());
					bis = new BufferedInputStream(fis, DECODE_BUFFER_SIZE);
					if (currentFile.getNumberOfSegments() == 1)
						ydecoder.setInputStream(bis, false);
					else
						ydecoder.setInputStream(bis, true);
					try {
						ydecoder.decode();
					} catch (YEncMissingTrailerException e){
						infoProcessor.debug("Ignored missing trailer exception");
					} catch (SizeException e){
						// Flush whats left in the buffer
						ydecoder.flushOutputStreamBuffer();
						infoProcessor.debug("Ignored size exception");
					}
					bis.close();
					fis.close();

				}
				bos.flush();
				bos.close();
				fos.close();

				// All decoded OK, remove all segments files
				segments = currentFile.getSegments().iterator();
				while (segments.hasNext()) {
					Segment segment = segments.next();
					java.io.File delSeg = new java.io.File(segment
							.getDownloadedSegmentFilename());
					delSeg.delete();
					delSeg = null;
				}
				infoProcessor.debug("Decoding complete");
				currentFile.setDecoded(true);
			} catch (FileNotFoundException e) {
				infoProcessor.error("Error loading segment - cannot decode - "
						+ e.getMessage());
			} catch (YEncException e) {
				infoProcessor.error("Error decoding file " + outputFilename);
				e.printStackTrace();
			} catch (IOException e) {
				infoProcessor.error("Error processing files for "
						+ outputFilename);
				e.printStackTrace();
			} finally {
				FileUtils.close(fis);
				FileUtils.close(bis);
				FileUtils.close(fos);
				FileUtils.close(bos);
			}

		} else {
			// TODO - might be able to decode, check what decoder does
			// Could have option to force - only do if the segment wasnt
			// available otherwise it might not have downloaded because of a
			// connection error and running again will get the segment
			this.infoProcessor
					.error("Not all segments downloaded - cannot decode");
		}

	}

	/**
	 * Retrieves the output filename from a segment. Will reset the ydecoder at
	 * the begining and end
	 * 
	 * @param segment
	 * @return The output filename from the segment
	 * @throws FileNotFoundException
	 */
	private String getOutputFilename(Segment segment)
			throws FileNotFoundException {
		this.ydecoder.reset();
		String outputFilename = null;
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(segment.getDownloadedSegmentFilename());
			ydecoder.setInputStream(fis);
			outputFilename = ydecoder.getFileName();
			fis.close();
		} catch (FileNotFoundException e) {
			infoProcessor.error("File not found getting output filename - "
					+ segment.getDownloadedSegmentFilename());
			throw e;
		} catch (IOException e) {
			// Cant get name, leave as null
		} catch (YEncException e) {
			// Cant get name, leave as null
		} finally {
			FileUtils.close(fis);
		}
		this.ydecoder.reset();
		return outputFilename;
	}
}